package dk.au.cs.peterbn.processing;

import com.google.common.collect.ImmutableSet;
import dk.au.cs.peterbn.annotations.*;
import dk.au.cs.peterbn.annotations.externals.ExternalProvider;
import dk.au.cs.peterbn.annotations.externals.ExternalTypeInformation;
import dk.au.cs.peterbn.patterns.DesignPattern;
import dk.au.cs.peterbn.patterns.PatternProvider;
import dk.au.cs.peterbn.patterns.TypeRole;
import dk.au.cs.peterbn.spoonprocessing.CallSiteProcessor;
import org.reflections.ReflectionUtils;
import org.reflections.Reflections;
import spoon.reflect.declaration.CtAnnotation;
import spoon.reflect.declaration.CtExecutable;
import spoon.reflect.declaration.CtType;
import spoon.reflect.reference.CtExecutableReference;
import spoon.reflect.reference.CtTypeReference;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import java.lang.annotation.Annotation;
import java.util.*;

import static dk.au.cs.peterbn.processing.Checks.assertAnnotationType;

/**
 * Utiltity functions for annotation processing, encapsulating a lot of environment data.
 * This may very well be the beginning of an instance of the BLOB anti pattern, although wikipedia tells
 * me it is actually sort of an <a href="http://en.wikipedia.org/wiki/Utility_pattern">Util pattern</a>.
 * It wraps several environment objects and performs a lot of heterogeneous operations using them.
 * <p/>
 * Contrary to the wikipedia description, this class is INTENDED to be instantiated, as it requires a lot of
 * environment information that it might as well carry around itself. All the functions are kinda static-like, though,
 * not manipulating (only reading) internal state.
 */
public class ProcessingUtil {

    private ProcessingEnvironment processingEnv;
    private Elements elementUtils;
    private Reflections reflections;
    private Types typeUtils;
    private PatternProvider patternProvider;
    private ExternalProvider externalProvider;
    private Map<String, PatternInstance> patternInstances;
    private boolean hasErrors;

    public ProcessingUtil(ProcessingEnvironment processingEnv, Elements elementUtils, Reflections reflections, Types typeUtils) {
        this.processingEnv = processingEnv;
        this.elementUtils = elementUtils;
        this.reflections = reflections;
        this.typeUtils = typeUtils;
        patternProvider = new PatternProvider();
        externalProvider = new ExternalProvider(patternProvider);
    }

    /**
     * Get all the pattern annotations available on an element as mirrors.
     *
     * @param element The element to look in
     * @return A potentially empty list of Pattern annotation mirrors
     */
    public List<AnnotationMirror> getPatternAnnotationMirrors(TypeElement element) {
        AnnotationMirror patterns = getAnnotationMirror(element, Patterns.class);
        AnnotationMirror pattern = getAnnotationMirror(element, Pattern.class);
        if (patterns != null && pattern != null) {
            printMessage(Diagnostic.Kind.WARNING, "Both Pattern and Patterns annotation present. Consider using only Patterns", element);
        }
        List<AnnotationMirror> mirrors = new ArrayList<AnnotationMirror>();
        if (patterns != null) {
            AnnotationValue value = getValueRaw(patterns, Patterns.Fields.VALUE);
            //noinspection unchecked
            mirrors.addAll((List<? extends AnnotationMirror>) value.getValue());
        }
        if (pattern != null) {
            mirrors.add(pattern);
        }
        return mirrors;
    }

    /**
     * Get java.lang representation of the Pattern annotations on a Class.
     *
     * @param withPattern The class to get the Pattern annotations on
     * @return All the Pattern annotations on the class, with those in a Patterns annotation unwrapped
     */
    public Set<Pattern> getAllPatternAnnotations(Class<?> withPattern) {
        Patterns annotation = withPattern.getAnnotation(Patterns.class);
        Set<Pattern> patternAnnotations = new HashSet<Pattern>();
        if (annotation != null) {
            patternAnnotations.addAll(Arrays.asList(annotation.value()));
        }
        Pattern pattern = withPattern.getAnnotation(Pattern.class);
        if (pattern != null) {
            patternAnnotations.add(pattern);
        }
        return patternAnnotations;
    }

    public AnnotationMirror getMemberAnnotation(ExecutableElement element, String annotationId) {
        Collection<AnnotationMirror> mirrors = getMemberAnnotations(element);
        for (AnnotationMirror mirror : mirrors) {
            if (getId(mirror).equals(annotationId)) {
                return mirror;
            }
        }
        return null;
    }

    /**
     * Get the set of Member annotations on a method
     *
     * @param element The element to get annotations for
     * @return The set of Member annotations
     */
    public Set<AnnotationMirror> getMemberAnnotations(ExecutableElement element) {
        AnnotationMirror membersMirror = getAnnotationMirror(element, Members.class);
        AnnotationMirror memberMirror = getAnnotationMirror(element, Member.class);
        if (membersMirror != null && memberMirror != null) {
            printMessage(Diagnostic.Kind.WARNING, "Both Member and Members annotation present. Consider using only Members", element);
        }
        Set<AnnotationMirror> mirrors = new HashSet<AnnotationMirror>();
        if (membersMirror != null) {
            Object value = getValueRaw(membersMirror, Members.Fields.VALUE).getValue();
            boolean value1 = value instanceof Collection;
            assert value1 : "Value of members annotation is not a collection";
            //noinspection unchecked
            mirrors.addAll((Collection<? extends AnnotationMirror>) value);
        }
        if (memberMirror != null) {
            mirrors.add(memberMirror);
        }
        return mirrors;
    }

    /**
     * Get a specific AnnotationMirror from an element
     *
     * @param element          Element to look in
     * @param targetAnnotation The target annotation
     * @return A mirror of the annotation or null if not found on the element
     */
    private AnnotationMirror getAnnotationMirror(Element element, Class<? extends Annotation> targetAnnotation) {
        List<? extends AnnotationMirror> allAnnotationMirrors = elementUtils.getAllAnnotationMirrors(element);
        for (AnnotationMirror mirror : allAnnotationMirrors) {
            if (mirror.getAnnotationType().toString().equals(targetAnnotation.getCanonicalName())) {
                return mirror;
            }
        }
        return null;
    }


    /**
     * Get the named property of the AnnotationMirror.
     *
     * @param annotation The AnnotationMirror to look in.
     * @param property   The name of the property to look for.
     * @return The property value.
     * @throws RuntimeException if the requested property name does not exist in the annotation type.
     */
    public String getValue(AnnotationMirror annotation, PropertyIdentifier property) {
        return getValueRaw(annotation, property).getValue().toString();
    }

    public String getId(AnnotationMirror annotation) {
        return getValue(annotation, Pattern.Fields.ID);
    }

    private AnnotationValue getValueRaw(AnnotationMirror annotation, PropertyIdentifier property) {
        Map<? extends ExecutableElement, ? extends AnnotationValue> valuesWithDefaults = elementUtils.getElementValuesWithDefaults(annotation);
        for (Map.Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : valuesWithDefaults.entrySet()) {
            ExecutableElement key = entry.getKey();
            if (key.getSimpleName().toString().equals(property.propName())) {
                return entry.getValue();
            }
        }
        throw new RuntimeException("Unknown annotation property: " + property + " on " + annotation.getAnnotationType().toString());
    }


    /**
     * @return Types annotated with Pattern or Patterns
     */
    public Set<Class<?>> getClassPathTypesWithPatterns() {
        Set<Class<?>> types = new HashSet<Class<?>>();
        types.addAll(getClassPathTypesAnnotatedWith(Pattern.class));
        types.addAll(getClassPathTypesAnnotatedWith(Patterns.class));
        return types;
    }

    private Set<Class<?>> getClassPathTypesAnnotatedWith(Class<? extends Annotation> patternClass) {
        Set<String> typesAnnotatedWith = reflections.getStore().getTypesAnnotatedWith(patternClass.getName());
        return ImmutableSet.copyOf(ReflectionUtils.forNames(typesAnnotatedWith));
    }


    /**
     * Loads the TypeRole element denoted by the role of the annotation on the element
     *
     * @param element    The annotated element
     * @param annotation The annotation
     * @return A TypeRole instance or <code>null</code> if it doesn't exist in the pattern indicated by the annotation
     */
    public TypeRole getTypeRole(Element element, AnnotationMirror annotation) {
        DesignPattern pattern = getDesignPattern(element, annotation);
        assertAnnotationType(annotation, Pattern.class);
        String roleName = getValue(annotation, Pattern.Fields.ROLE);
        if (pattern != null) {
            return pattern.getTypeRole(roleName);
        } else {
            return null;
        }
    }


    /**
     * Loads the DesignPattern element denoted by the name of the annotation on the element
     *
     * @param element    The annotated element. Used for error reporting.
     * @param annotation The annotation
     * @return A DesignPattern instance or <code>null</code> if it couldn't be loaded and instantiated
     */
    public DesignPattern getDesignPattern(Element element, AnnotationMirror annotation) {
        assertAnnotationType(annotation, Pattern.class);
        //We need the raw value for error reporting
        AnnotationValue nameValue = getValueRaw(annotation, Pattern.Fields.NAME);
        if (nameValue != null) {
            String identifier = nameValue.getValue().toString();
            try {
                return patternProvider.getDesignPattern(identifier);
            } catch (Exception e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, e.toString(), element, annotation, nameValue);
            }
        }
        return null;
    }

    private void setError(Diagnostic.Kind kind) {
        if (Diagnostic.Kind.ERROR == kind) {
            hasErrors = true;
        }
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public void printMessage(Diagnostic.Kind error, String message) {
        setError(error);
        processingEnv.getMessager().printMessage(error, message);
    }

    public void printMessage(Diagnostic.Kind error, String msg, Element element) {
        setError(error);
        processingEnv.getMessager().printMessage(error, msg, element);
    }

    public void printMessage(Diagnostic.Kind error, String message, Element element, AnnotationMirror annotation) {
        setError(error);
        processingEnv.getMessager().printMessage(error, message, element, annotation);
    }

    @SuppressWarnings({"UnusedDeclaration"})
    private void printMessage(Diagnostic.Kind error, String message, Element element, AnnotationMirror annotation, AnnotationValue value) {
        setError(error);
        processingEnv.getMessager().printMessage(error, message, element, annotation, value);
    }


    public List<? extends Element> getAllMembers(TypeElement typeElement) {
        return elementUtils.getAllMembers(typeElement);
    }

    public boolean hasOverride(ExecutableElement typeMember) {
        return getAnnotationMirror(typeMember, Override.class) != null;
    }

    public boolean overrides(ExecutableElement overrider, ExecutableElement overridden, TypeElement type) {
        return elementUtils.overrides(overrider, overridden, type);
    }

    public TypeElement asTypeElement(TypeMirror typeMirror) {
        return (TypeElement) typeUtils.asElement(typeMirror);
    }

    public Set<ExternalTypeInformation> getExternalInformation(String id) {
        try {
            return externalProvider.getExternalsIn(id);
        } catch (Exception e) {
            printMessage(Diagnostic.Kind.ERROR, "Unable to load external information: " + e.toString());
            return new HashSet<ExternalTypeInformation>();
        }
    }

    public PatternInstance getPatternInstance(String id) {
        return patternInstances.get(id);
    }

    public void setPatternInstances(Map<String, PatternInstance> patternInstances) {
        this.patternInstances = patternInstances;
    }

    public Set<CtAnnotation<Member>> getAllDeclaredMemberAnnotations(CtExecutableReference<?> executableReference) {
        Set<CtAnnotation<Member>> memberRoles = new HashSet<CtAnnotation<Member>>();
        CtExecutable<?> ctExecutable = executableReference.getDeclaration();
        if (ctExecutable != null) {
            //It may be null if we hit compiled code
            Set<CtAnnotation<? extends Annotation>> annotations = ctExecutable.getAnnotations();
            for (CtAnnotation<? extends Annotation> annotation : annotations) {
                CtTypeReference<? extends Annotation> type = annotation.getAnnotationType();
                if (CallSiteProcessor.MEMBER_REF.equals(type)) {
                    memberRoles.add((CtAnnotation<Member>) annotation);
                } else if (CallSiteProcessor.MEMBERS_REF.equals(type)) {
                    memberRoles.addAll(Members.Fields.getValue((CtAnnotation<Members>) annotation));
                }
            }
        }
        return memberRoles;
    }

    public boolean noErrors() {
        return !hasErrors;
    }

    public Set<CtAnnotation<Pattern>> getAllPatternAnnotations(CtType<?> containingType) {
        HashSet<CtAnnotation<Pattern>> patterns = new HashSet<CtAnnotation<Pattern>>();
        Set<CtAnnotation<? extends Annotation>> annotations = containingType.getAnnotations();
        for (CtAnnotation<? extends Annotation> annotation : annotations) {
            CtTypeReference<? extends Annotation> type = annotation.getAnnotationType();
            if (CallSiteProcessor.PATTERN_REF.equals(type)) {
                patterns.add((CtAnnotation<Pattern>) annotation);
            } else if (CallSiteProcessor.PATTERNS_REF.equals(type)) {
                List<CtAnnotation<Pattern>> annotationList = Patterns.Fields.getValue((CtAnnotation<Patterns>) annotation);
                patterns.addAll(annotationList);
            }
        }
        return patterns;
    }
    
    
    public Types Types() {
        return  typeUtils;
    }
}
